import { Module } from './modules/module'
import { vnode, VNode } from './vnode'
import * as is from './is'
import { htmlDomApi, DOMAPI } from './htmldomapi'

type NonUndefined<T> = T extends undefined ? never : T

function isUndef (s: any): boolean {
  return s === undefined
}
function isDef<A> (s: A): s is NonUndefined<A> {
  return s !== undefined
}

type VNodeQueue = VNode[]

const emptyNode = vnode('', {}, [], undefined, undefined)

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined
}

type KeyToIndexMap = {[key: string]: number}

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
}

type ModuleHooks = ArraysOf<Required<Module>>

function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  const map: KeyToIndexMap = {}
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key
    if (key !== undefined) {
      map[key] = i
    }
  }
  return map
}

const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number
  let j: number
  const cbs: ModuleHooks = {
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: []
  }
  // 初始化转换虚拟节点的 api

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
  // 把传入的所有模块的钩子函数，统一存储到 csb 对象中
  // 最终构建的 cbs 对象的形式 cbs = { create: [fn1, fn2], update: [], ... }
  for (i = 0; i < hooks.length; ++i) {
    // cbs.create = []  cbs.update = [] ....
    cbs[hooks[i]] = []
    // modules 传入的模块数组
    for (j = 0; j < modules.length; ++j) {
      // 获取模块中的hook函数（钩子函数）
      // hook = modules[0][create] ....
      const hook = modules[j][hooks[i]]
      if (hook !== undefined) {
        // 把获取到的hook函数放到 cbs 对应的钩子函数数组中
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }
  // 把 dom 转换成 vnode
  function emptyNodeAt (elm: Element) {
    const id = elm.id ? '#' + elm.id : ''
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
  }

  function createRmCb (childElm: Node, listeners: number) {
    // 返回删除元素的函数
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm) as Node
        api.removeChild(parent, childElm)
      }
    }
  }
  // 创建 vnode 对应的dom元素，并触发 int/create 钩子函数
  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any
    let data = vnode.data
    if (data !== undefined) {
      // 执行用户设置的 init 钩子函数
      const init = data.hook?.init
      if (isDef(init)) {
        init(vnode)
        data = vnode.data
      }
    }
    // 把 vnode 转换成真是 dom 对象（没有渲染到页面上）
    const children = vnode.children
    const sel = vnode.sel
    // 创建注释节点
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = ''
      }
      vnode.elm = api.createComment(vnode.text!)
    } else if (sel !== undefined) {
      // Parse selector
      // 找到ID的下标
      const hashIdx = sel.indexOf('#')
      // 找到class的下标
      const dotIdx = sel.indexOf('.', hashIdx)

      const hash = hashIdx > 0 ? hashIdx : sel.length
      const dot = dotIdx > 0 ? dotIdx : sel.length
      // 获取标签
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag)
        // 判断有ID，怎设置ID属性
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
      // 如果有class的下标，则设置class属性
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
      // 触发模块的 create周期函数
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      // 如果有子 vnode 资源，则循环调用 createElm 方法
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i]
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
      // 如果text不为空，则添加标签内容
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      const hook = vnode.data!.hook
      // 触发用户设置的create函数
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode)
        if (hook.insert) {
          insertedVnodeQueue.push(vnode)
        }
      }
    } else {
      vnode.elm = api.createTextNode(vnode.text!)
    }
    // 返回创建的 dom
    return vnode.elm
  }

  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
      }
    }
  }
  // 触发 destroy 钩子函数
  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data
    if (data !== undefined) {
      // 执行用户传入 destroy 钩子函数
      data?.hook?.destroy?.(vnode)
      // 执行模块的 destroy 钩子函数
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
      // 执行自元素的 destroy 钩子函数
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j]
          if (child != null && typeof child !== 'string') {
            invokeDestroyHook(child)
          }
        }
      }
    }
  }
  // 删除节点
  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number
      let rm: () => void
      const ch = vnodes[startIdx]
      if (ch != null) {
        if (isDef(ch.sel)) {
          // 执行 destroy 钩子函数（会执行所有子节点的 destroy 钩子函数）
          invokeDestroyHook(ch)
          listeners = cbs.remove.length + 1
          rm = createRmCb(ch.elm!, listeners)
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          // 执行用户设置的 remove 钩子函数
          const removeHook = ch?.data?.hook?.remove
          if (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            // 如果用户没有设置钩子函数，则直接调用删除元素的方法
            rm()
          }
        } else { // Text node
          // 如果是文本节点，则直接调用删除方法
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }
  // diff算法
  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let newEndIdx = newCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any
    // 每轮比较最多处理一个节点，算法复杂度 O(n)
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果进行比较的 4 个节点中存在空节点，为空的节点下标向中间推进，继续下个循环
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 从左对比
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 从右对比
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 最左侧 对比 最右侧
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 旧开始节点等于新的结束节点，说明节点向右移动了
        // 具体移动到哪，因为新节点处于末尾，所以添加到旧结束节点（会随着 updateChildren 左移）的后面
        // 注意这里需要移动 dom，因为节点右移了，而为什么是插入 oldEndVnode 的后面呢？
        // 可以分为两个情况来理解：
        // 1. 当循环刚开始，下标都还没有移动，那移动到 oldEndVnode 的后面就相当于是最后面，是合理的
        // 2. 循环已经执行过一部分了，因为每次比较结束后，下标都会向中间靠拢，而且每次都会处理一个节点,
        // 这时下标左右两边已经处理完成，可以把下标开始到结束区域当成是并未开始循环的一个整体，
        // 所以插入到 oldEndVnode 后面是合理的（在当前循环来说，也相当于是最后面，同 1）
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 最右侧 对比 最右侧
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 旧的结束节点等于新的开始节点，说明节点是向左移动了，逻辑同上
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 如果以上 4 种情况都不匹配，可能存在下面 2 种情况
        // 1. 这个节点是新创建的
        // 2. 这个节点在原来的位置是处于中间的（oldStartIdx 和 endStartIdx之间）

        // 如果 oldKeyToIdx 不存在，创建 key 到 index 的映射
        // 只会创建一次，并且已经完成的部分不需要映射
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        // 拿到在 oldCh 下对应的下标
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        // 如果下标不存在，说明这个节点是新创建的
        if (isUndef(idxInOld)) { // New element
          // 插入到 oldStartVnode 的前面（对于当前循环来说，相当于最前面）
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // 如果是已经存在的节点 找到需要移动位置的节点
          elmToMove = oldCh[idxInOld]
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            // 在 oldCh 中将当前已经处理的 vnode 置空，等下次循环到这个下标的时候直接跳过
            oldCh[idxInOld] = undefined as any
            // 插入到 oldStartVnode 的前面（对于当前循环来说，相当于最前面）
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 循环结束后，可能会存在两种情况
    // 1. oldCh 已经全部处理完成，而 newCh 还有新的节点，需要对剩下的每个项都创建新的 dom
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        // 如果老数组先到达末尾，说明新数组还有更多的元素，这些元素都是新增的，说以一次性插入
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
        // 2. newCh 已经全部处理完成，而 oldCh 还有旧的节点，需要将多余的节点移除
        // 如果新数组先到达末尾，说明新数组比老数组少了一些元素，所以一次性删除
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 首先执行用户设置的 prepatch 钩子函数
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    // 判断内存地址（相同，则直接返回）
    if (oldVnode === vnode) return
    if (vnode.data !== undefined) {
      // 执行模块的 update 钩子函数
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 执行用户的 update 钩子函数
      vnode.data.hook?.update?.(oldVnode, vnode)
    }
    // 如果 text 未定义
    if (isUndef(vnode.text)) {
      // 如果新老节点都有 children
      if (isDef(oldCh) && isDef(ch)) {
        // 使用diff 算法对比子节点，更新子节点
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      } else if (isDef(ch)) {
        // 如果新节点有 children 老节点没有 children
        // 如果老节点有 text 则清空dom元素内容
        if (isDef(oldVnode.text)) api.setTextContent(elm, '')
        // 批量添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      if (isDef(oldCh)) {
        // 如果老节点有 children， 移除
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // 设置dom元素的文本内容
      api.setTextContent(elm, vnode.text!)
    }
    // 执行用户传入的 postpatch 钩子函数
    hook?.postpatch?.(oldVnode, vnode)
  }
  // init 内部返回 patch 函数，把 vnode 渲染成真是 DOM，并返回 vnode
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    // 保存新插入节点的队列，为了触发钩子函数
    const insertedVnodeQueue: VNodeQueue = []
    // 执行模块的 pro 钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    // 如果 oldVnode 不是 Vnode ，创建 Vnode 并设置elm
    if (!isVnode(oldVnode)) {
      // 把dom节点转换成空的 Vnode
      oldVnode = emptyNodeAt(oldVnode)
    }
    // 如果新旧节点是相同节点（key 和 sel 相同）
    // 比较语义不同的结构没有意义,比如diff一个'div'和'span'
    // 而应该移除div,根据span vnode插入新的span
    // diff两个key不相同的vnode同样没有意义
    // 指定key就是为了区分element
    // 对于不同key的element,不应该去根据newVnode来改变oldVnode的数据
    // 而应该移除oldVnode,添加newVnode
    if (sameVnode(oldVnode, vnode)) {
      // oldVnode与vnode的sel和key分别相同,那么这两个vnode值得去比较
      // 找到节点的差异并更新 DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      // 如果新旧节点不同， vnode 创建对应的dom
      // 获取当前dom元素
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node
      // 创建 vnode 对应的dom元素，并触发 int/create 钩子函数
      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
        // 如果父节点不为空，把 vnode 对应的dom插入到文档中
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 移除老节点
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }
    // 执行用户设置的 insert 钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    // 执行模块的 post 钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
  }
}
